package com.seckill.service.serviceImpl;

import com.seckill.cache.RedisDao;
import com.seckill.dao.SeckillDao;
import com.seckill.dao.SuccessDao;
import com.seckill.dto.Exposer;
import com.seckill.dto.SeckillExecution;
import com.seckill.entity.Seckill;
import com.seckill.entity.Success;
import com.seckill.exception.seckillException.RepeatKillException;
import com.seckill.exception.seckillException.SeckillCloseException;
import com.seckill.exception.seckillException.SeckillException;
import com.seckill.service.SeckillService;
import com.seckill.util.SeckillStatusEnum;
import org.apache.commons.collections.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @Creation_time 2017/9/7
 * @Theme Test
 * @Autor Xiao Yiyun
 */

@Service
public class SeckillServiceImpl implements SeckillService {

    //相关dao层接口对象
    @Autowired
    private SeckillDao seckillDao;
    @Autowired
    private SuccessDao successDao;
    @Autowired
    private RedisDao redisDao;
    //日志对象
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    //md5加密需要的盐
    private final String salt = "hello world";

    //根据Id查询秒杀信息
    public Seckill selectSeckillById(int seckill_id) {
        Seckill seckill = null;
        try {
            seckill = seckillDao.selectSeckillById(seckill_id);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return seckill;
    }

    //查询所有秒杀商品信息
    public List<Seckill> selectAllSeckill(int offset, int limit) {
        List<Seckill> seckills = null;
        try {
            seckills = seckillDao.selectAllSeckill(offset, limit);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return seckills;
    }

    //暴露秒杀URL信息
    public Exposer exportSeckillUrl(int seckillId){
        //1.通过Id得到秒杀商品数据库参数
        Seckill seckill = null;
        try {
            //优化点：通过Redis缓存进行优化处理,维护一致性，建立在超时的基础上
            //1.通过Redis获取信息
            seckill = redisDao.getSeckill(seckillId);
            if (seckill == null) {
                //2.如果缓存中没有，则从数据库中获取
                seckill = seckillDao.selectSeckillById(seckillId);
                if (seckill == null) {
                    return new Exposer(false, seckillId);
                } else {
                    //3.如果从数据库中得到seckill对象，存入缓存中
                    redisDao.setSeckill(seckill);
                }
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        //2.如果通过Id无法查到秒杀商品，返回对象标识为不能暴露URL和Id
        if (seckill == null) {
            return new Exposer(false, seckillId);
        }
        //3.如果当前系统时间不在秒杀范围内，返回对象标识不能暴露URL和当前，开始，结束时间
        Date startTime = seckill.getSeckill_start_time();
        Date endTime = seckill.getSeckill_end_time();
        Date nowTime = new Date();
        if (nowTime.getTime()<startTime.getTime() || nowTime.getTime()>endTime.getTime()) {
            return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());
        }
        //4.符合秒杀条件，返回对象标识为可以暴露URL，md5加密路径和秒杀商品id
        String md5 = getMd5(seckillId);
        return new Exposer(true, seckillId, md5);
    }

    //执行秒杀操作,md5验证用户的URL是否合法
    @Transactional
    /*
    * 使用注解控制事务方法的优点：
    *   1）开发团队达成一致约定，明确标注事务方法的编程风格
    *   2）保证事务方法的执行时间尽可能短，不要穿插其他的网络操作RPC/HTTP请求，或者剥离到方法外
    *   3）不是所有的方法都需要事务，如只有一条修改操作，只读操作等
    * */
    public SeckillExecution executeSeckill(int seckillId, String userPhone, String md5)
            throws SeckillException, SeckillCloseException, RepeatKillException {
        SeckillExecution seckillExecution = null;
        try {
            //1.判断md5是否合法，证明用户是否修改了URL
            if (md5==null || !md5.equals(getMd5(seckillId))) {
                throw new SeckillException("seckill url data rewrite");
            }
            //3.记录购买行为,调整顺序，只进行一次commit/rollback
            int insertCountRes = successDao.insertSuccess(seckillId, userPhone);
            if (insertCountRes <= 0) {
                //写入失败，重复秒杀
                throw new RepeatKillException("seckill repeated");
            } else {
                //2.进行减库存处理，热点商品竞争
                Date nowTime = new Date();
                int updateCountRes = seckillDao.reduceSeckillCount(seckillId, nowTime);
                if (updateCountRes <= 0) {
                    //没有更新到记录，秒杀结束，rollback
                    throw new SeckillCloseException("seckill is closed");
                } else {
                    //4.秒杀成功，返回秒杀成功的购买记录,commit
                    Success success = successDao.selectSuccessBySeckillId(seckillId);
                    seckillExecution = new SeckillExecution(seckillId, SeckillStatusEnum.SUCCESS, success);
                }
            }
        } catch (SeckillCloseException e1) {
            throw e1;
        } catch (RepeatKillException e2) {
            throw e2;
        } catch (Exception e) {
            //使用日志进行回滚
            logger.error(e.getMessage(), e);
            //将所有检查异常转化成运行时异常
            throw new SeckillException("seckill inner error：" + e.getMessage());
        }
        return seckillExecution;
    }

    public SeckillExecution executeSeckillProcedure(
            int seckillId, String userPhone, String md5) {
        if (md5 == null || !md5.equals(getMd5(seckillId))) {
            return new SeckillExecution(seckillId, SeckillStatusEnum.DATA_REWRITE);
        }
        Date killTime = new Date();
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("seckillId", seckillId);
        map.put("userPhone", userPhone);
        map.put("killTime", killTime);
        map.put("result", null);
        try {
            seckillDao.killByProcedure(map);
            //获取result
            int result = MapUtils.getInteger(map, "result", -2);
            if (result == 1) {
                Success success = successDao.selectSuccessBySeckillId(seckillId);
                return new SeckillExecution(seckillId, SeckillStatusEnum.SUCCESS, success);
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            return new SeckillExecution(seckillId, SeckillStatusEnum.INNER_ERROR);
        }
        return null;
    }

    //私有工具方法，生成md5加密
    private String getMd5(int seckillId) {
        String base = seckillId + "/" + this.salt;
        //Spring自带的md5生成工具方法
        String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
        return md5;
    }
}
